/*
 * Copyright 2017 Google
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#import <XCTest/XCTest.h>
#import <Foundation/Foundation.h>
#import "FPersist.h"
#import "FIRDatabaseReference.h"
#import "FIRDatabaseReference_Private.h"
#import "FRepo_Private.h"
#import "FTestHelpers.h"
#import "FDevice.h"
#import "FIRDatabaseQuery_Private.h"

@implementation FPersist

- (void) setUp {
    [super setUp];

    NSFileManager *fileManager = [NSFileManager defaultManager];

    NSString *baseDir = [FPersist getFirebaseDir];
    // HACK: We want to clean up old persistence files from previous test runs, but on OSX, baseDir is going to be something
    // like /Users/michael/Documents/firebase, and we probably shouldn't blindly delete it, since somebody might have actual
    // documents there.  We should probably change the directory where we store persistence on OSX to .firebase or something
    // to avoid colliding with real files, but for now, we'll leave it and just manually delete each of the /0, /1, /2, etc.
    // directories that may exist from previous test runs.  As of now (2014/09/07), these directories only go up to ~50, but
    // if we add a ton more tests, we may need to increase the 100.  But I'm guessing we'll rewrite persistence and move the
    // persistence folder before then though.
    for(int i = 0; i < 100; i++) {
        // TODO: This hack is uneffective because the format now follows different rules. Persistence really needs a purge
        // option
        NSString *dir = [NSString stringWithFormat:@"%@/%d", baseDir, i];
        if ([fileManager fileExistsAtPath:dir]) {
            NSError *error;
            [[NSFileManager defaultManager] removeItemAtPath:dir error:&error];
            if (error) {
                XCTFail(@"Failed to clear persisted data at %@: %@", dir, error);
            }
        }
    }
}

- (void) testSetIsResentAfterRestart {
    FIRDatabaseReference *readerRef = [FTestHelpers getRandomNode];
    NSString *url = [readerRef description];
    FDevice* device = [[FDevice alloc] initOfflineWithUrl:url];

    // Monitor the data at this location.
    __block FIRDataSnapshot *readSnapshot = nil;
    [readerRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
        readSnapshot = snapshot;
    }];

    // Do some sets while offline and then "kill" the app, so it doesn't get sent to Firebase.
    [device do:^(FIRDatabaseReference *ref) {
        [ref setValue:@{ @"a": @42, @"b": @3.1415, @"c": @"hello", @"d": @{ @"dd": @"dd-val", @".priority": @"d-pri"} }];
        [[ref child:@"a"] setValue:@"a-val"];
        [[ref child:@"c"] setPriority:@"c-pri"];
        [ref updateChildValues:@{ @"b": @"b-val"}];
    }];

    // restart and wait for "idle" (so all pending puts should have been sent).
    [device restartOnline];
    [device waitForIdleUsingWaiter:self];

    // Pending sets should have gone through.
    id expected = @{
            @"a": @"a-val",
            @"b": @"b-val",
            @"c": @{ @".value": @"hello", @".priority": @"c-pri" },
            @"d": @{ @"dd": @"dd-val", @".priority": @"d-pri" }
    };
    [self waitForExportValueOf:readerRef toBe:expected];

    // Set the value to something else (12).
    [readerRef setValue:@12];

    // "restart" the app again and make sure it doesn't set it to 42 again.
    [device restartOnline];
    [device waitForIdleUsingWaiter:self];

    // Make sure data is still 12.
    [self waitForRoundTrip:readerRef];
    XCTAssertEqual(readSnapshot.value, @12, @"Read data should still be 12.");
    [device dispose];
}

- (void) testSetIsReappliedAfterRestart {
    FDevice* device = [[FDevice alloc] initOffline];

    // Do some sets while offline and then "kill" the app, so it doesn't get sent to Firebase.
    [device do:^(FIRDatabaseReference *ref) {
        [ref setValue:@{ @"a": @42, @"b": @3.1415, @"c": @"hello" }];
        [[ref child:@"a"] setValue:@"a-val"];
        [[ref child:@"c"] setPriority:@"c-pri"];
        [ref updateChildValues:@{ @"b": @"b-val"}];
    }];

    // restart the app offline and observe the data.
    [device restartOffline];

    // Pending sets should be visible
    id expected = @{
            @"a": @"a-val",
            @"b": @"b-val",
            @"c": @{ @".value": @"hello", @".priority": @"c-pri" }
    };
    [device do:^(FIRDatabaseReference *ref) {
        [self waitForExportValueOf:ref toBe:expected];
    }];
    [device dispose];
}

- (void) testServerDataCachedOffline1 {
    FIRDatabaseReference *writerRef = [FTestHelpers getRandomNode];
    FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writerRef description] ];
    __block BOOL done = NO;
    id data = @{@"a": @1, @"b": @2};
    [writerRef setValue:data withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
        done = YES;
    }];
    WAIT_FOR(done);

    // Wait for the data to get it cached.
    [device do:^(FIRDatabaseReference *ref) {
        [self waitForValueOf:ref toBe:data];
    }];

    // Should still be there after restart, offline.
    [device restartOffline];
    [device do:^(FIRDatabaseReference *ref) {
        [self waitForValueOf:ref toBe:data];
    }];

    // Children should be there too.
    [device restartOffline];
    [device do:^(FIRDatabaseReference *ref) {
        [self waitForValueOf:[ref child:@"a"] toBe:@1];
    }];
    [device dispose];
}

- (void) testServerDataCompleteness1 {
    FIRDatabaseReference *writerRef = [FTestHelpers getRandomNode];
    FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writerRef description] ];
    id data = @{@"child": @{@"a": @1, @"b": @2 }, @"other": @"blah"};
    [self waitForCompletionOf:writerRef setValue:data];

    // Wait for each child to get it cached (but not the parent).
    [device do:^(FIRDatabaseReference *ref) {
        [self waitForValueOf:[ref child:@"child/a"] toBe:@1];
        [self waitForValueOf:[ref child:@"child/b"] toBe:@2];
        [self waitForValueOf:[ref child:@"other"] toBe:@"blah"];
    }];

    // Restart, offline, should get child_added events, but not value.
    [device restartOffline];
    __block BOOL gotA, gotB;
    [device do:^(FIRDatabaseReference *ref) {
        FIRDatabaseReference *childRef = [ref child:@"child"];
        [childRef observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
            if ([snapshot.key isEqualToString:@"a"]) {
                XCTAssertEqualObjects(snapshot.value, @1, @"Got a");
                gotA = YES;
            } else if ([snapshot.key isEqualToString:@"b"]) {
                XCTAssertEqualObjects(snapshot.value, @2, @"Got a");
                gotB = YES;
            } else {
                XCTFail(@"Unexpected child event.");
            }
        }];

        // Listen for value events (which we should *not* get).
        [childRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
            XCTFail(@"Got a value event with incomplete data!");
        }];

        // Wait for another location just to make sure we wait long enough that we /would/ get a value event if it
        // was coming.
        [self waitForValueOf:[ref child:@"other"] toBe:@"blah"];
    }];

    XCTAssertTrue(gotA && gotB, @"Got a and b.");
    [device dispose];
}

- (void) testServerDataCompleteness2 {
    FIRDatabaseReference *writerRef = [FTestHelpers getRandomNode];
    FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writerRef description] ];
    id data = @{@"a": @1, @"b": @2};
    [self waitForCompletionOf:writerRef setValue:data];

    // Wait for the children individually.
    [device do:^(FIRDatabaseReference *ref) {
        [self waitForValueOf:[ref child:@"a"] toBe:@1];
        [self waitForValueOf:[ref child:@"b"] toBe:@2];
    }];

    // Should still be there after restart, offline.
    [device restartOffline];
    [device do:^(FIRDatabaseReference *ref) {
        [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
            // No-op.  Just triggering a listen at this location.
        }];
        [self waitForValueOf:[ref child:@"a"] toBe:@1];
        [self waitForValueOf:[ref child:@"b"] toBe:@2];
    }];
    [device dispose];
}

- (void)testServerDataLimit {
    FIRDatabaseReference *writerRef = [FTestHelpers getRandomNode];
    FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writerRef description] ];
    [self waitForCompletionOf:writerRef setValue:@{@"a": @1, @"b": @2, @"c": @3}];

    // Cache limit(2) of the data.
    [device do:^(FIRDatabaseReference *ref) {
        FIRDatabaseQuery *limitRef = [ref queryLimitedToLast:2];
        [self waitForValueOf:limitRef toBe:@{@"b": @2, @"c": @3 }];
    }];

    // We should be able to get limit(2) data offline, but not the whole node.
    [device restartOffline];
    [device do:^(FIRDatabaseReference *ref) {
        [ref observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
            XCTFail(@"Got value event for whole node!");
        }];

        FIRDatabaseQuery *limitRef = [ref queryLimitedToLast:2];
        [self waitForValueOf:limitRef toBe:@{@"b": @2, @"c": @3 }];
    }];
    [device dispose];
}

- (void)testRemoveWhileOfflineAndRestart {
    FIRDatabaseReference *writerRef = [FTestHelpers getRandomNode];
    FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writerRef description] ];

    [[writerRef child:@"test"] setValue:@"test"];
    [device do:^(FIRDatabaseReference *ref) {
        // Cache this location.
        __block id val = nil;
        [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
            val = snapshot.value;
        }];
        [self waitUntil:^BOOL {
            return [val isEqual:@{@"test": @"test"}];
        }];
    }];
    [device restartOffline];

    __block BOOL done = NO;
    [writerRef removeValueWithCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
        done = YES;
    }];
    WAIT_FOR(done);

    [device goOnline];
    [device waitForIdleUsingWaiter:self];
    [device do:^(FIRDatabaseReference *ref) {
        [self waitForValueOf:ref toBe:[NSNull null]];
    }];
    [device dispose];
}


- (void)testDeltaSyncAfterRestart {
    FIRDatabaseReference *writerRef = [FTestHelpers getRandomNode];
    FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writerRef description] ];

    [writerRef setValue:@"test"];

    [device do:^(FIRDatabaseReference *ref) {
        // Cache this location.
        __block id val = nil;
        [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
            val = snapshot.value;
        }];
        [self waitUntil:^BOOL {
            return [val isEqual:@"test"];
        }];
        XCTAssertEqual(ref.repo.dataUpdateCount, 1L, @"Should have gotten one update.");
    }];
    [device restartOnline];

    [device waitForIdleUsingWaiter:self];
    [device do:^(FIRDatabaseReference *ref) {
        [self waitForValueOf:ref toBe:@"test"];
        XCTAssertEqual(ref.repo.dataUpdateCount, 0L, @"Should have gotten no updates.");
    }];
    [device dispose];
}

- (void)testDeltaSyncWorksWithUnfilteredQuery {
    FIRDatabaseReference *writerRef = [FTestHelpers getRandomNode];
    FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writerRef description] ];

    // List must be large enough to trigger delta sync.
    NSMutableDictionary *longList = [[NSMutableDictionary alloc] init];
    for(NSInteger i = 0; i < 50; i++) {
        NSString *key = [[writerRef childByAutoId] key];
        longList[key] = @{ @"order": @1, @"text": @"This is an awesome message!" };
    }

    [writerRef setValue:longList];

    [device do:^(FIRDatabaseReference *ref) {
        // Cache this location.
        [self waitForValueOf:[ref queryOrderedByChild:@"order"] toBe:longList];
        XCTAssertEqual(ref.repo.dataUpdateCount, 1L, @"Should have gotten one update.");
    }];
    [device restartOffline];

    // Add a new child while the device is offline.
    FIRDatabaseReference *newChildRef = [writerRef childByAutoId];
    NSDictionary *newChild = @{ @"order": @50, @"text": @"This is a new appended child!" };

    [self waitForCompletionOf:newChildRef setValue:newChild];
    longList[[newChildRef key]] = newChild;

    [device goOnline];
    [device do:^(FIRDatabaseReference *ref) {
        // Wait for updated value with new child.
        [self waitForValueOf:[ref queryOrderedByChild:@"order"] toBe:longList];
        XCTAssertEqual(ref.repo.rangeMergeUpdateCount, 1L, @"Should have gotten a range merge update.");
    }];
    [device dispose];
}

- (void) testPutsAreRestoredInOrder {
    FDevice *device = [[FDevice alloc] initOffline];

    // Store puts which should have a putId with 10 which is lexiographical small than 9
    [device do:^(FIRDatabaseReference *ref) {
        for (int i = 0; i < 11; i++) {
            [ref setValue:[NSNumber numberWithInt:i]];
        }
    }];

    // restart the app offline and observe the data.
    [device restartOffline];

    // Make sure that the write with putId 10 wins, not 9
    id expected = @10;
    [device do:^(FIRDatabaseReference *ref) {
        [self waitForExportValueOf:ref toBe:expected];
    }];
    [device dispose];
}

- (void) testStoreSetsPerf1 {
    if (!runPerfTests) return;
    // Disable persistence in FDevice for comparison without persistence
    FDevice *device = [[FDevice alloc] initOnline];

    __block BOOL done = NO;
    [device do:^(FIRDatabaseReference *ref) {
        NSDate *start = [NSDate date];
        [self writeChildren:ref count:1000 size:100 waitForComplete:NO];

        [self waitForQueue:ref];

        NSLog(@"Elapsed: %f", [[NSDate date] timeIntervalSinceDate:start]);
        done = YES;
    }];

    WAIT_FOR(done);
    [device dispose];
}

- (void) testStoreListenPerf1 {
    if (!runPerfTests) return;
    // Disable persistence in FDevice for comparison without persistence

    // Write 1000 x 100-byte children, to read back.
    unsigned int count = 1000;
    FIRDatabaseReference *writer = [FTestHelpers getRandomNode];
    [self writeChildren:writer count:count size:100];

    FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writer description]];

    __block BOOL done = NO;
    [device do:^(FIRDatabaseReference *ref) {
        NSDate *start = [NSDate date];
        [ref observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
            // Wait to make sure we're done persisting everything.
            [self waitForQueue:ref];
            XCTAssertEqual(snapshot.childrenCount, count, @"Got correct data.");
            NSLog(@"Elapsed: %f", [[NSDate date] timeIntervalSinceDate:start]);
            done = YES;
        }];
    }];

    WAIT_FOR(done);
    [device dispose];
}

- (void) testRestoreListenPerf1 {
    if (!runPerfTests) return;

    // NOTE: Since this is testing restoration of data from cache after restarting, it only works with persistence on.

    // Write 1000 * 100-byte children, to read back.
    unsigned int count = 1000;
    FIRDatabaseReference *writer = [FTestHelpers getRandomNode];
    [self writeChildren:writer count:count size:100];

    FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writer description]];

    // Get the data cached.
    __block BOOL done = NO;
    [device do:^(FIRDatabaseReference *ref) {
        [ref observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
            XCTAssertEqual(snapshot.childrenCount, count, @"Got correct data.");
            done = YES;
        }];
    }];
    WAIT_FOR(done);

    // Restart offline and see how long it takes to restore the data from cache.
    [device restartOffline];
    done = NO;
    [device do:^(FIRDatabaseReference *ref) {
        NSDate *start = [NSDate date];
        [ref observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
            // Wait to make sure we're done persisting everything.
            XCTAssertEqual(snapshot.childrenCount, count, @"Got correct data.");
            [self waitForQueue:ref];
            NSLog(@"Elapsed: %f", [[NSDate date] timeIntervalSinceDate:start]);
            done = YES;
        }];
    }];

    WAIT_FOR(done);
    [device dispose];
}

- (void)writeChildren:(FIRDatabaseReference *)writer count:(unsigned int)count size:(unsigned int)size {
    [self writeChildren:writer count:count size:size waitForComplete:YES];
}

- (void)writeChildren:(FIRDatabaseReference *)writer count:(unsigned int)count size:(unsigned int)size waitForComplete:(BOOL)waitForComplete {
    __block BOOL done = NO;

    NSString *data = [self randomStringOfLength:size];
    for(int i = 0; i < count; i++) {
        [[writer childByAutoId] setValue:data withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
            if (i == (count - 1)) {
                done = YES;
            }
        }];
    }
    if (waitForComplete) {
        WAIT_FOR(done);
    }
}

NSString *letters = @"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
- (NSString*) randomStringOfLength:(unsigned int)len {
    NSMutableString *randomString = [NSMutableString stringWithCapacity: len];

    for (int i=0; i<len; i++) {
        [randomString appendFormat: @"%C", [letters characterAtIndex: arc4random() % [letters length]]];
    }
    return randomString;
}

+ (NSString *) getFirebaseDir {
    NSArray *dirPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDir = [dirPaths objectAtIndex:0];
    NSString *firebaseDir = [documentsDir stringByAppendingPathComponent:@"firebase"];

    return firebaseDir;
}

@end
